package org.bgee.psp4jdbc.interceptor;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.tomcat.jdbc.pool.ConnectionPool;
import org.apache.tomcat.jdbc.pool.JdbcInterceptor;
import org.apache.tomcat.jdbc.pool.PooledConnection;
import org.apache.tomcat.jdbc.pool.PoolProperties.InterceptorProperty;
import org.bgee.psp4jdbc.Properties;
import org.bgee.psp4jdbc.jdbcapi.CallableStatement;
import org.bgee.psp4jdbc.jdbcapi.PreparedStatement;
import org.bgee.psp4jdbc.pool.Pool;
import org.bgee.psp4jdbc.pool.PoolMonitor;
import org.bgee.psp4jdbc.pool.ReleaseException;

/**
 * A <code>JdbcInterceptor</code> making use of the library <code>psp4jdbc</code> 
 * to perform <code>PreparedStatement/CallableStatement</code> pooling. 
 * The advantage over other <code>PreparedStatement</code> pool interceptor 
 * is that it provides a LFU and LRU eviction mechanism.
 * 
 * @author Frederic Bastian
 * @version 1.0
 * @since 1.0
 */
public class Psp4jdbcInterceptor extends JdbcInterceptor {

	/**
     * <code>java.sql.Connection#prepareStatement</code> method names.
     */
    public static final String PREPARESTATEMENT_VAL = "prepareStatement";
	/**
     * <code>java.sql.Connection#prepareCall</code> method names.
     */
    public static final String PREPARECALL_VAL = "prepareCall";
    
    /**
     * A psp4jdbc <code>Properties</code> object associated to this 
     * <code>JdbcInterceptor</code>.
     */
    private final Properties properties;
    
    /**
     * The <code>Pool</code> of <code>PreparedStatement</code>s. Will be set on 
     * the first invocation to {@link #reset(ConnectionPool, PooledConnection)}, 
     * so that we make sure properties are already set. 
     */
    private Pool pool;
    
    public Psp4jdbcInterceptor() {
    	super();
    	this.properties = new Properties();
    	this.pool = null;
    }
    
    /**
     * This implementation processes this method call only if name of <code>method</code> 
     * is {@link #PREPARECALL_VAL} or {@link #PREPARESTATEMENT_VAL}, otherwise, it simply 
     * delegates the call to its parent. When this implementation processes this query, 
     * it returns a psp4jdbc <code>PreparedStatement</code> or 
     * <code>CallableStatement</code>, either newly instantiated, or retrieved from 
     * the psp4jdbc PreparedStatement pool. These <code>PreparedStatement</code>s 
     * return to their pool with parameters cleared when <code>close</code> is called on them, 
     * rather than actually closing the underlying <code>java.sql.PreparedStatement</code>.
     * 
     * @param proxy 	see {@link JdbcInterceptor#invoke(Object, Method, Object[])}.
     * @param method	see {@link JdbcInterceptor#invoke(Object, Method, Object[])}.
     * @param args		see {@link JdbcInterceptor#invoke(Object, Method, Object[])}.
     * @return 			An <code>Object</code> that is either the <code>Object</code> 
     * 					returned by the parent of this class, or a psp4jdbc 
     * 					<code>PreparedStatement</code> if this instance processed 
     * 					the call.
     */
	@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		
		if (this.compare(PREPARECALL_VAL, method) || 
				this.compare(PREPARESTATEMENT_VAL, method)) {
			if (this.pool == null) {
				throw new IllegalStateException(
						"The psp4jdbc pool was not correctly initialized");
			}
			//try to retrieve the statement from the pool
			String key = this.generateKey(method, args);
			PreparedStatement ps =  (PreparedStatement) this.pool.remove(key);
	        if (ps != null && !ps.getRealStatement().isClosed()) {
	            return ps;
	        }
	        
	        //otherwise, get it from the connection
	        if (this.compare(PREPARESTATEMENT_VAL, method)) {
	        	return new PreparedStatement(key, this.pool, (java.sql.Connection) proxy, 
	        			(java.sql.PreparedStatement) super.invoke(proxy,method,args));
	        } 
	        return new CallableStatement(key, this.pool, (java.sql.Connection) proxy, 
	        			(java.sql.CallableStatement) super.invoke(proxy,method,args));
		} 
		return super.invoke(proxy, method, args);
	}
	
	/**
	 * Generate a key to perform a cache lookup based on parameters provided to 
	 * {@link #invoke(Object, Method, Object[])}.
	 * @param method	The <code>Method</code> used in the <code>invoke</code> call.
	 * @param args		The <code>Object</code> <code>array</code> used 
	 * 					in the <code>invoke</code> call.
	 * @return			A <code>String</code> that will allow to retrieve 
	 * 					a <code>PreparedStatement</code> or <code>CallableStatement</code> 
	 * 					from the pool.
	 */
	private String generateKey(Method method, Object[] args) {
		//we use a hash function to reduce memory usage for long SQL query.
		//the counter side is that it could increase memory usage if all queries were short.
		//The DigestUtils lib is already part of psp4jdbc by default.
		return DigestUtils.sha256Hex(method.getName() + " - " + 
	        Arrays.deepToString(args));
	}
	
	@Override
	public void reset(ConnectionPool parent, PooledConnection con) {
		if ((parent != null || con!= null) && this.pool == null) {
			this.pool = new Pool(this.properties.getPoolMaxSize(), 
					this.properties.getEvictionMethod(), 
					this.properties.getEvictionFactor());
		}
	}
	
	/**
	 * Release the psp4jdbc <code>Pool</code> associated to this interceptor.
	 */
	@Override
	public void disconnected(ConnectionPool parent, PooledConnection con, 
			boolean finalizing) {
		if (this.pool != null) {
			try {
			    this.pool.close();
			    this.pool = null;
			} catch (ReleaseException e) {
				//at this point, if an Exception is thrown, 
				//it should be captured by the tomcat-jdbc lib, 
				//as it would result from a call on an underlying 
				//tomcat-jdbc PreparedStatement. 
				//So, we do nothing.
			}
		}
		super.disconnected(parent, con, finalizing);
    }
	
	/**
	 * Release all psp4jdbc <code>Pool</code>s in this class loader.
	 */
	@Override
	public void poolClosed(ConnectionPool pool) {
        try {
			PoolMonitor.close();
		} catch (ReleaseException e) {
			//at this point, if an Exception is thrown, 
			//it should be captured by the tomcat-jdbc lib, 
			//as it would result from a call on an underlying 
			//tomcat-jdbc PreparedStatement. 
			//So, we do nothing.
		}
        super.poolClosed(pool);
    }
	
	@Override
	/*
	 * (non-Javadoc)
	 * Nothing to be done yet
	 */
	public void poolStarted(ConnectionPool pool) {
        super.poolStarted(pool);
    }
	
	@Override
	public void setProperties(Map<String, InterceptorProperty> properties) {
        super.setProperties(properties);
        for (InterceptorProperty prop: properties.values()) {
        	this.properties.setAttribute(prop.getName(), prop.getValue());
        }
    }

}
